﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using PortalCurriculo.Util.PasswordComplexity;

namespace PortalCurriculo.Util.Security
{
   /// <summary>
   /// Used to test password strengths
   /// </summary>
   public class SimplePasswordStrength : IPassword
   {
      #region Data Tables used to detect Sequences
      static readonly char[][] sequences = {
                                           // Alphabetic sequence
                                            new char[] { '0', '1', '2' , '3', '4', '5', '6', '7', '8', '9',
                                                        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
                                                        'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
                                                        'v', 'w', 'x', 'y', 'z'
                                                      }
                                           // qwertyUsKeyBoard sequence
                                           ,new char[] { 'q', 'w', 'e' , 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\',
                                                         'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'',
                                                         'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
                                                         '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+'
                                                       }
                                           // qwertyUkKeyBoard sequence
                                           ,new char[] { 'q', 'w', 'e' , 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\',
                                                         'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'',
                                                         'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
                                                         '!', '\"', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+'
                                                       }
                                           // azertyKeyBoard sequence
                                           ,new char[] { 'a', 'z', 'e' , 'r', 't', 'y', 'u', 'i', 'o', 'p', '-', '\\',
                                                         'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'',
                                                         'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
                                                         '&', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+'
                                                       }
                                           };
      #endregion

      /// <summary>
      /// Checks the password for well known sequences.
      /// </summary>
      /// <param name="password">The password.</param>
      /// <returns></returns>
      static public int CheckPasswordForSequences(string password)
      {
         int maxLength = password.Length;
         string lowerCheck = password.ToLower();
         int calcLength;

         foreach (char[] sequence in sequences)
         {
            calcLength = CheckSequence(lowerCheck, sequence);
            if (calcLength < maxLength)
            {
               maxLength = calcLength;
            }
         }

         return maxLength;
      }

      /// <summary>
      /// Returns length of password after sequences have been removed
      /// </summary>
      /// <param name="password">The password.</param>
      /// <param name="sequenceArray">The sequence array.</param>
      /// <returns></returns>
      static public int CheckSequence(string password, char[] sequenceArray)
      {
         if (password == null)
         {
            throw new ArgumentNullException("password", "Password needs to be supplied");
         }

         if (sequenceArray == null)
         {
            throw new ArgumentNullException("sequenceArray", "sequenceArray needs to be supplied");
         }

         int sequenceCounter = 0;
         int lastIndex = -2;
         int sequenceFalsePositiveSupressionThreshold = 1;
         int sequenceSupressionCounter = 0;

         foreach (char character in password)
         {
            int indexInArray = Array.IndexOf<char>(sequenceArray, character);

            if ((indexInArray > -1) && ((indexInArray == lastIndex)
                                         || (indexInArray == lastIndex + 1)
                                         || (indexInArray == lastIndex - 1))
               )
            {
               // Only count the information as being sequenced if we have at least 3
               // characters as being in sequence. Without this safe check we cause
               // problems with longer passwords having accidental sequences triggering
               // a failure in policies
               if (sequenceSupressionCounter == sequenceFalsePositiveSupressionThreshold)
               {
                  sequenceCounter += sequenceFalsePositiveSupressionThreshold + 1;
               }
               if (sequenceSupressionCounter > sequenceFalsePositiveSupressionThreshold)
               {
                  sequenceCounter++;
               }
               sequenceSupressionCounter++;
            }
            else
            {
               sequenceSupressionCounter = 0;
            }

            lastIndex = indexInArray;
         }

         return password.Length - sequenceCounter;
      }

      /// <summary>
      /// Simple substitution for removing Elite encoding.
      /// </summary>
      /// <remarks>
      /// 1337/Leet is a common substition approach.
      /// </remarks>
      /// <param name="password">The password.</param>
      /// <returns></returns>
      static public string Simple1337Decode(string password)
      {
         string deLeeted = password.Replace('@', 'a')
            //.Replace('6', 'b')
                                   .Replace('8', 'b')
                                   .Replace('[', 'c')
                                   .Replace('(', 'c')
                                   .Replace('3', 'e')
                                   .Replace('6', 'g')
                                   .Replace('9', 'g')
                                   .Replace('#', 'h')
                                   .Replace('!', 'i')
                                   .Replace('1', 'i')
                                   .Replace('1', 'l')
                                   .Replace(@"/\/\", "m")
                                   .Replace(@"|\/|", "m")
                                   .Replace(@"(\/)", "m")
                                   .Replace(@"|\|", "n")
                                   .Replace('0', 'o')
                                   .Replace('5', 's')
                                   .Replace('$', 's')
                                   .Replace('7', 't')
                                   .Replace('+', 't')
                                   .Replace(@"\/\/", "w")
                                   .Replace(@"\/", "v")
                                   .Replace('2', 'z')
                                   ;

         return deLeeted;
      }

      #region IPassword Members

      /// <summary>
      /// Given a password it attempts to determine its strength based on entropy and effective length
      /// </summary>
      /// <param name="password">The password.</param>
      /// <returns>Strength indicator</returns>
      /// <remarks>
      /// Strength is ultimately deceided by the object
      /// implementing the password processors
      /// </remarks>
      public PasswordStrengthIndex GetPasswordStrength(string password)
      {
         int bitStrength = GetPasswordPseudoEntropy(password);

         if (bitStrength < 10)
         {
            return PasswordStrengthIndex.None;
         }

         if (bitStrength < 30)
         {
            return PasswordStrengthIndex.Weak;
         }

         if (bitStrength < 40)
         {
            return PasswordStrengthIndex.Medium;
         }

         if (bitStrength < 60)
         {
            return PasswordStrengthIndex.Strong;
         }

         return PasswordStrengthIndex.MostStrong;
      }

      /// <summary>
      /// Gets the password entropy per character.
      /// </summary>
      /// <param name="password">The password to tbe determined.</param>
      /// <returns>Max number of entropy bits per character in the password</returns>
      static public double GetPasswordEntropyPerCharacter(string password)
      {
         int maxCombinationsPerCharacterInPassword = 0;
         bool lowerCaseCharactersDetected = false;
         bool upperCaseCharactersDetected = false;
         bool digitsDetected = false;
         bool specialCharactersDetected = false;

         foreach (char character in password)
         {
            lowerCaseCharactersDetected |= char.IsLower(character);
            upperCaseCharactersDetected |= char.IsUpper(character);
            digitsDetected |= char.IsNumber(character);
            specialCharactersDetected |= (!char.IsLetter(character) && !char.IsNumber(character));
         }

         if (lowerCaseCharactersDetected) maxCombinationsPerCharacterInPassword+=26;
         if (upperCaseCharactersDetected) maxCombinationsPerCharacterInPassword+=26;
         if (digitsDetected) maxCombinationsPerCharacterInPassword+=10;
         if (specialCharactersDetected) maxCombinationsPerCharacterInPassword+=32;

         return Math.Log(maxCombinationsPerCharacterInPassword,2);
      }

      #region Simple lightweight array of common passwords
      /// <summary>
      /// Very small simple dictionary for trivial dictionary testing
      /// </summary>
      private string[] _commonPasswordList = 
      {
         "iloveyou",
         "ilove",
         "baseball",
         "asshole",
         "flower",
         "buster",
         "pepper",
         "secret",
         "hello", 
         "there", 
         "password",
         "jesus", 
         "love", 
         "princess", 
         "blessed", 
         "sunshine", 
         "freedom",
         "heaven", 
         "blessing", 
         "shadow", 
         "tigger", 
         "summer",
         "letmein", 
         "test", 
         "trustno", 
         "dragon", 
         "monkey", 
         "master", 
         "killer",
         "computer", 
         "starwars", 
         "starwarz", 
         "warez", 
         "cheese", 
         "matrix",
         "football", 
         "blahblah", 
         "fuckyou", 
         "fucku", 
         "helpme", 
         "biteme",
         "696969",
         "mustang",
         "marlboro",
         "myspace"
      };
      #endregion

      /// <summary>
      /// Internal private dictionary
      /// </summary>
      private Collection<string> _dictionary = new Collection<string>();

      /// <summary>
      /// Resets the dictionary to load well known passwords.
      /// </summary>
      private void ResetDictionaryToLoadWellKnownPasswords()
      {
         _dictionary.Clear();

         foreach (string word in _commonPasswordList)
         {
            _dictionary.Add(word);
         }
      }

      /// <summary>
      /// Gets the simple password dictionary.
      /// </summary>
      /// <value>The dictionary.</value>
      public Collection<string> Dictionary
      {
         get
         {
            if (_dictionary.Count == 0)
            {
               ResetDictionaryToLoadWellKnownPasswords();
            }
            return _dictionary;
         }
      }

      /// <summary>
      /// Checks password for dictionary entries
      /// </summary>
      /// <param name="password">The password to check.</param>
      /// <returns>Max length of matching dictionary entry</returns>
      public int LongestMatchingDictionaryEntry(string password)
      {
         int maxDictionaryMatch = 0;

         // Check length against any dictionary values. We record the longest match for any one word
         // we are not trying to remove contatenated words as this is both quite hard and the fact
         // is that contatenated dictionary words are harder to brute. So we will just stick to the
         // values found in the dictionary (which may have concatentated words :-))
         string dictionaryCheckPassword = Simple1337Decode(password).ToLower();
         foreach (string word in Dictionary)
         {
            if (dictionaryCheckPassword.Contains(word))
            {
               if (word.Length > maxDictionaryMatch)
               {
                  maxDictionaryMatch = word.Length;
               }
            }
         }

         return maxDictionaryMatch;
      }

      /// <summary>
      /// Returns an approximation to the entropy for the supplied password
      /// </summary>
      /// <param name="password">The password.</param>
      /// <returns>Calculated strength of password in bits</returns>
      /// <remarks>
      /// Note as this is relatively subjective (unless the password is random)
      /// so it rarely will make sense to compare values between different
      /// password strength providers
      /// </remarks>
      public int GetPasswordPseudoEntropy(string password)
      {
         // First get the length of the password, correcting for any sequences
         int getMiniumNonSequencedLength = CheckPasswordForSequences(password.ToLower());
         int maxDictionaryMatch = 0;

         maxDictionaryMatch = LongestMatchingDictionaryEntry(password);

         // Determine the max entropy for each character
         double entropyPerCharacter = GetPasswordEntropyPerCharacter(password);

         // return the strength removing any sequences and the biggest dictionary word
         return Convert.ToInt32(entropyPerCharacter * (getMiniumNonSequencedLength-maxDictionaryMatch));
      }

      public bool CheckPasswordMeetsPolicyRequirements(string password, IPasswordPolicy policy)
      {
         return CheckPasswordMeetsPolicyRequirements(string.Empty, password, policy);
      }

      /// <summary>
      /// Determines is a segment of the user name in present in the supplied password.
      /// </summary>
      /// <param name="password">The password.</param>
      /// <param name="name">The name.</param>
      /// <param name="minimumMatchCount">The minimum number of characters to match to the password</param>
      /// <returns></returns>
      static public bool FindNameInPassword(string password, string name, int minimumMatchCount)
      {
         if (minimumMatchCount <= 0)
         {
            throw new ArgumentException("MinimumMatchCount must be greater than or equal to 1.", "minimumMatchCount");
         }

         // If we have an empty passwor or the name is empty we are returning we found
         // it as this will violate the policy.
         if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(name))
         {
            return true;
         }

         if (name.Length < minimumMatchCount)
         {
            return false;
         }

         bool matchFound = false;
         string lowerPassword = password.ToLower();
         string lowerName = name.ToLower();
         int searchSegmentCount = lowerName.Length - minimumMatchCount + 1;

         for (int searchIndex = 0; searchIndex < searchSegmentCount; searchIndex++)
         {
            if (lowerPassword.Contains(lowerName.Substring(searchIndex, minimumMatchCount)))
            {
               return true;
            }
         }
         return matchFound;
      }

      /// <summary>
      /// Checks the password meets policy strength against the policy and verified the user name is not in the password
      /// </summary>
      /// <param name="userName">The username.</param>
      /// <param name="password">The password.</param>
      /// <param name="policy">The policy.</param>
      /// <returns>
      /// True if password meets the policy, false on failure
      /// </returns>
      public bool CheckPasswordMeetsPolicyRequirements(string userName, string password, IPasswordPolicy policy)
      {
         bool meetsPolicyRequirements = true;
         bool sequenceDetected = false;
         bool lowerCaseCharactersDetected = false;
         bool upperCaseCharactersDetected = false;
         bool digitsDetected = false;
         bool specialCharactersDetected = false;

         foreach (char character in password)
         {
            lowerCaseCharactersDetected |= char.IsLower(character);
            upperCaseCharactersDetected |= char.IsUpper(character);
            digitsDetected |= char.IsNumber(character);
            specialCharactersDetected |= (!char.IsLetter(character) && !char.IsNumber(character));
         }

         int effectiveLength = CheckPasswordForSequences(password);
         if (effectiveLength + policy.MaximumCharacterSequenceCount <= password.Length)
         {
            sequenceDetected = true;
         }

         // Only continue to process is we dont have a detected sequence
         if (
             (sequenceDetected)
             || password.Length < policy.MinimumPasswordLength
             || password.Length > policy.MaximumPasswordLength
             || (policy.RequireDigits && !digitsDetected)
             || (policy.RequireMixedCase && !(lowerCaseCharactersDetected && upperCaseCharactersDetected))
             || (policy.RequireSpecialCharacters && !specialCharactersDetected)
             || GetPasswordStrength(password) < policy.MinimumPasswordStrengthIndex
             || (policy.RequireUserNameToCheckPasswordAgainst && FindNameInPassword(password, userName, 4))
            )
         {
            meetsPolicyRequirements = false;
         }

         return meetsPolicyRequirements;
      }

      #endregion
   }
}
